diff --git a/api/server.go b/api/server.go index ad16470a..898f85a9 100644 --- a/api/server.go +++ b/api/server.go @@ -88,12 +88,16 @@ func (s *Server) setupRoutes() { // 系统提示词模板管理(无需认证) api.GET("/prompt-templates", s.handleGetPromptTemplates) api.GET("/prompt-templates/:name", s.handleGetPromptTemplate) + + // 公开的竞赛数据(无需认证) + api.GET("/traders", s.handlePublicTraderList) + api.GET("/competition", s.handlePublicCompetition) // 需要认证的路由 protected := api.Group("/", s.authMiddleware()) { // AI交易员管理 - protected.GET("/traders", s.handleTraderList) + protected.GET("/my-traders", s.handleTraderList) protected.GET("/traders/:id/config", s.handleGetTraderConfig) protected.POST("/traders", s.handleCreateTrader) protected.PUT("/traders/:id", s.handleUpdateTrader) @@ -115,8 +119,6 @@ func (s *Server) setupRoutes() { protected.POST("/user/signal-sources", s.handleSaveUserSignalSource) - // 竞赛总览 - protected.GET("/competition", s.handleCompetition) // 指定trader的数据(使用query参数 ?trader_id=xxx) protected.GET("/status", s.handleStatus) @@ -1455,3 +1457,62 @@ func (s *Server) handleGetPromptTemplate(c *gin.Context) { "content": template.Content, }) } + +// handlePublicTraderList 获取公开的交易员列表(无需认证) +func (s *Server) handlePublicTraderList(c *gin.Context) { + // 从所有用户获取交易员信息 + competition, err := s.traderManager.GetCompetitionData() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("获取交易员列表失败: %v", err), + }) + return + } + + // 获取traders数组 + tradersData, exists := competition["traders"] + if !exists { + c.JSON(http.StatusOK, []map[string]interface{}{}) + return + } + + traders, ok := tradersData.([]map[string]interface{}) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "交易员数据格式错误", + }) + return + } + + // 返回交易员基本信息,过滤敏感信息 + result := make([]map[string]interface{}, 0, len(traders)) + for _, trader := range traders { + result = append(result, map[string]interface{}{ + "trader_id": trader["trader_id"], + "trader_name": trader["trader_name"], + "ai_model": trader["ai_model"], + "exchange": trader["exchange"], + "is_running": trader["is_running"], + "total_equity": trader["total_equity"], + "total_pnl": trader["total_pnl"], + "total_pnl_pct": trader["total_pnl_pct"], + "position_count": trader["position_count"], + "margin_used_pct": trader["margin_used_pct"], + }) + } + + c.JSON(http.StatusOK, result) +} + +// handlePublicCompetition 获取公开的竞赛数据(无需认证) +func (s *Server) handlePublicCompetition(c *gin.Context) { + competition, err := s.traderManager.GetCompetitionData() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("获取竞赛数据失败: %v", err), + }) + return + } + + c.JSON(http.StatusOK, competition) +} diff --git a/web/public/images/hand-bg.png b/web/public/images/hand-bg.png new file mode 100644 index 00000000..8ca9e333 Binary files /dev/null and b/web/public/images/hand-bg.png differ diff --git a/web/public/images/hand.png b/web/public/images/hand.png new file mode 100644 index 00000000..619da0ae Binary files /dev/null and b/web/public/images/hand.png differ diff --git a/web/public/images/logo.png b/web/public/images/logo.png deleted file mode 100644 index 28ec8c71..00000000 Binary files a/web/public/images/logo.png and /dev/null differ diff --git a/web/src/App.tsx b/web/src/App.tsx index 6f785908..9335c132 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -7,6 +7,8 @@ import { LoginPage } from './components/LoginPage'; import { RegisterPage } from './components/RegisterPage'; import { CompetitionPage } from './components/CompetitionPage'; import { LandingPage } from './pages/LandingPage'; +import HeaderBar from './components/landing/HeaderBar'; +import LoginModal from './components/landing/LoginModal'; import AILearning from './components/AILearning'; import { LanguageProvider, useLanguage } from './contexts/LanguageContext'; import { AuthProvider, useAuth } from './contexts/AuthContext'; @@ -43,30 +45,44 @@ function App() { const { user, token, logout, isLoading } = useAuth(); const { config: systemConfig, loading: configLoading } = useSystemConfig(); const [route, setRoute] = useState(window.location.pathname); + const [showLoginModal, setShowLoginModal] = useState(false); - // 从URL hash读取初始页面状态(支持刷新保持页面) + // 从URL路径读取初始页面状态(支持刷新保持页面) const getInitialPage = (): Page => { + const path = window.location.pathname; const hash = window.location.hash.slice(1); // 去掉 # - return hash === 'trader' || hash === 'details' ? 'trader' : 'competition'; + + if (path === '/traders' || hash === 'traders') return 'traders'; + if (path === '/dashboard' || hash === 'trader' || hash === 'details') return 'trader'; + return 'competition'; // 默认为竞赛页面 }; const [currentPage, setCurrentPage] = useState(getInitialPage()); const [selectedTraderId, setSelectedTraderId] = useState(); const [lastUpdate, setLastUpdate] = useState('--:--:--'); - // 监听URL hash变化,同步页面状态 + // 监听URL变化,同步页面状态 useEffect(() => { - const handleHashChange = () => { + const handleRouteChange = () => { + const path = window.location.pathname; const hash = window.location.hash.slice(1); - if (hash === 'trader' || hash === 'details') { + + if (path === '/traders' || hash === 'traders') { + setCurrentPage('traders'); + } else if (path === '/dashboard' || hash === 'trader' || hash === 'details') { setCurrentPage('trader'); - } else if (hash === 'competition' || hash === '') { + } else if (path === '/competition' || hash === 'competition' || hash === '') { setCurrentPage('competition'); } + setRoute(path); }; - window.addEventListener('hashchange', handleHashChange); - return () => window.removeEventListener('hashchange', handleHashChange); + window.addEventListener('hashchange', handleRouteChange); + window.addEventListener('popstate', handleRouteChange); + return () => { + window.removeEventListener('hashchange', handleRouteChange); + window.removeEventListener('popstate', handleRouteChange); + }; }, []); // 切换页面时更新URL hash (当前通过按钮直接调用setCurrentPage,这个函数暂时保留用于未来扩展) @@ -166,153 +182,137 @@ function App() { return () => window.removeEventListener('popstate', handlePopState); }, []); + // Set current page based on route for consistent navigation state + useEffect(() => { + if (route === '/competition') { + setCurrentPage('competition'); + } else if (route === '/traders') { + setCurrentPage('traders'); + } else if (route === '/dashboard') { + setCurrentPage('trader'); + } + }, [route]); + // Show loading spinner while checking auth or config if (isLoading || configLoading) { return (
- NoFx Logo + NoFx Logo

{t('loading', language)}

); } - // Show landing page for root route when not authenticated + // Handle specific routes regardless of authentication + if (route === '/login') { + return ; + } + if (route === '/register') { + return ; + } + if (route === '/competition') { + return ( +
+ setShowLoginModal(true)} + isLoggedIn={!!user} + currentPage="competition" + language={language} + onLanguageChange={setLanguage} + user={user} + onLogout={logout} + isAdminMode={systemConfig?.admin_mode} + onPageChange={(page) => { + console.log('Competition page onPageChange called with:', page); + console.log('Current route:', route, 'Current page:', currentPage); + + if (page === 'competition') { + console.log('Navigating to competition'); + window.history.pushState({}, '', '/competition'); + setRoute('/competition'); + setCurrentPage('competition'); + } else if (page === 'traders') { + console.log('Navigating to traders'); + window.history.pushState({}, '', '/traders'); + setRoute('/traders'); + setCurrentPage('traders'); + } else if (page === 'trader') { + console.log('Navigating to trader/dashboard'); + window.history.pushState({}, '', '/dashboard'); + setRoute('/dashboard'); + setCurrentPage('trader'); + } + + console.log('After navigation - route:', route, 'currentPage:', currentPage); + }} + /> +
+ +
+
+ ); + } + + // Show landing page for root route + if (route === '/' || route === '') { + return ; + } + + // Show main app for authenticated users on other routes if (!systemConfig?.admin_mode && (!user || !token)) { - if (route === '/login') { - return ; - } - if (route === '/register') { - return ; - } - // Default to landing page when not authenticated + // Default to landing page when not authenticated and no specific route return ; } return ( -
- {/* Header - Binance Style */} -
-
-
- {/* Left - Logo and Title */} -
-
- NOFX -
-
-

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

-

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

-
-
- - {/* Center - Page Toggle (absolutely positioned) */} -
- - - -
- - {/* Right - Actions */} -
- - {/* User Info - Only show if not in admin mode */} - {!systemConfig?.admin_mode && user && ( -
-
- {user.email[0].toUpperCase()} -
- {user.email} -
- )} - - {/* Admin Mode Indicator */} - {systemConfig?.admin_mode && ( -
- - {t('adminMode', language)} -
- )} - - {/* Language Toggle */} -
- - -
- - {/* Logout Button - Only show if not in admin mode */} - {!systemConfig?.admin_mode && ( - - )} -
-
-
-
+
+ setShowLoginModal(true)} + isLoggedIn={!!user} + isHomePage={false} + currentPage={currentPage} + language={language} + onLanguageChange={setLanguage} + user={user} + onLogout={logout} + isAdminMode={systemConfig?.admin_mode} + onPageChange={(page) => { + console.log('App.tsx onPageChange called with:', page); + console.log('Current route:', route, 'Current page:', currentPage); + + if (page === 'competition') { + console.log('Navigating to competition'); + window.history.pushState({}, '', '/competition'); + setRoute('/competition'); + setCurrentPage('competition'); + } else if (page === 'traders') { + console.log('Navigating to traders'); + window.history.pushState({}, '', '/traders'); + setRoute('/traders'); + setCurrentPage('traders'); + } else if (page === 'trader') { + console.log('Navigating to trader/dashboard'); + window.history.pushState({}, '', '/dashboard'); + setRoute('/dashboard'); + setCurrentPage('trader'); + } + + console.log('After navigation - route:', route, 'currentPage:', currentPage); + }} + /> {/* Main Content */} -
+
{currentPage === 'competition' ? ( ) : currentPage === 'traders' ? ( { setSelectedTraderId(traderId); + window.history.pushState({}, '', '/dashboard'); + setRoute('/dashboard'); setCurrentPage('trader'); }} /> diff --git a/web/src/components/CompetitionPage.tsx b/web/src/components/CompetitionPage.tsx index 1ebdb564..2e3d90f6 100644 --- a/web/src/components/CompetitionPage.tsx +++ b/web/src/components/CompetitionPage.tsx @@ -31,6 +31,8 @@ export function CompetitionPage() { setIsModalOpen(true); } catch (error) { console.error('Failed to fetch trader config:', error); + // 对于未登录用户,不显示详细配置,这是正常行为 + // 竞赛页面主要用于查看排行榜和基本信息 } }; diff --git a/web/src/components/CryptoFeatureCard.tsx b/web/src/components/CryptoFeatureCard.tsx index 9c78960a..0affa99d 100644 --- a/web/src/components/CryptoFeatureCard.tsx +++ b/web/src/components/CryptoFeatureCard.tsx @@ -30,8 +30,8 @@ export const CryptoFeatureCard = React.forwardRef {/* Icon container */} -
{icon}
+
{icon}
{/* Title */} -

{title}

+

{title}

{/* Description */} -

{description}

+

{description}

{/* Features list */}
@@ -95,11 +95,11 @@ export const CryptoFeatureCard = React.forwardRef
-
- +
+
- {feature} + {feature} ))}
diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index 48dc6a5b..06352dee 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -15,7 +15,7 @@ export function Header({ simple = false }: HeaderProps) { {/* Left - Logo and Title */}
- NoFx Logo + NoFx Logo

diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx index ea36356c..9040fba1 100644 --- a/web/src/components/LoginPage.tsx +++ b/web/src/components/LoginPage.tsx @@ -2,8 +2,7 @@ import React, { useState } from 'react'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; -import { Header } from './Header'; -import { ArrowLeft } from 'lucide-react'; +import HeaderBar from './landing/HeaderBar'; export function LoginPage() { const { language } = useLanguage(); @@ -51,43 +50,44 @@ export function LoginPage() { }; return ( -
-
+
+ {}} + isLoggedIn={false} + isHomePage={false} + currentPage="login" + language={language} + onLanguageChange={(lang) => {}} + onPageChange={(page) => { + console.log('LoginPage onPageChange called with:', page); + if (page === 'competition') { + window.location.href = '/competition'; + } + }} + /> -
+
- {/* Back to Home */} - {/* Logo */}
- NoFx Logo + NoFx Logo
-

- {t('loginTitle', language)} +

+ 登录 NOFX

-

- {step === 'login' ? t('loginTitle', language) : t('enterOTPCode', language)} +

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

{/* Login Form */} -
+
{step === 'login' ? (
-
-
{error && ( -
+
{error}
)} @@ -126,7 +126,7 @@ export function LoginPage() { type="submit" disabled={loading} className="w-full px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50" - style={{ background: '#F0B90B', color: '#000' }} + style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }} > {loading ? t('loading', language) : t('loginButton', language)} @@ -142,7 +142,7 @@ export function LoginPage() {
-
{error && ( -
+
{error}
)} @@ -168,7 +168,7 @@ export function LoginPage() { type="button" onClick={() => setStep('login')} className="flex-1 px-4 py-2 rounded text-sm font-semibold" - style={{ background: '#2B3139', color: '#848E9C' }} + style={{ background: 'var(--panel-bg-hover)', color: 'var(--text-secondary)' }} > {t('back', language)} @@ -187,17 +187,17 @@ export function LoginPage() { {/* Register Link */}
-

- {t('noAccount', language)}{' '} +

+ 还没有账户?{' '}

diff --git a/web/src/index.css b/web/src/index.css index cc360ff5..46756529 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -5,11 +5,16 @@ @tailwind components; @tailwind utilities; +/* 强制显示滚动条以确保所有页面布局一致 */ +html { + overflow-y: scroll; +} + :root { /* Binance Brand Colors */ --brand-yellow: #F0B90B; - --brand-black: #0C0E12; - --brand-dark-gray: #1E2329; + --brand-black: #000000; + --brand-dark-gray: #0A0A0A; --brand-light-gray: #EAECEF; --brand-almost-white: #FAFAFA; --brand-white: #FFFFFF; @@ -20,14 +25,14 @@ --binance-yellow-light: #FCD535; --binance-yellow-glow: rgba(240, 185, 11, 0.2); - --background: #181A20; /* Binance body bg */ - --header-bg: #0B0E11; /* Binance header bg */ - --background-elevated: #181A20; + --background: #000000; /* Binance body bg */ + --header-bg: #000000; /* Binance header bg */ + --background-elevated: #000000; --foreground: #EAECEF; - --panel-bg: #1E2329; - --panel-bg-hover: #252930; - --panel-border: #2B3139; - --panel-border-hover: #474D57; + --panel-bg: #0A0A0A; + --panel-bg-hover: #111111; + --panel-border: #1A1A1A; + --panel-border-hover: #2A2A2A; /* Binance Signature Colors */ --binance-green: #0ECB81; @@ -44,7 +49,7 @@ --text-disabled: #474D57; /* Chart Colors */ - --grid-stroke: #2B3139; + --grid-stroke: #1A1A1A; --axis-tick: #5E6673; --ref-line: #474D57; diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index c6b0c87c..6009b0a7 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -32,13 +32,20 @@ function getAuthHeaders(): Record { export const api = { // AI交易员管理接口 async getTraders(): Promise { - const res = await fetch(`${API_BASE}/traders`, { + const res = await fetch(`${API_BASE}/my-traders`, { headers: getAuthHeaders(), }); if (!res.ok) throw new Error('获取trader列表失败'); return res.json(); }, + // 获取公开的交易员列表(无需认证) + async getPublicTraders(): Promise { + const res = await fetch(`${API_BASE}/traders`); + if (!res.ok) throw new Error('获取公开trader列表失败'); + return res.json(); + }, + async createTrader(request: CreateTraderRequest): Promise { const res = await fetch(`${API_BASE}/traders`, { method: 'POST', @@ -252,11 +259,9 @@ export const api = { return res.json(); }, - // 获取竞赛数据 + // 获取竞赛数据(无需认证) async getCompetition(): Promise { - const res = await fetch(`${API_BASE}/competition`, { - headers: getAuthHeaders(), - }); + const res = await fetch(`${API_BASE}/competition`); if (!res.ok) throw new Error('获取竞赛数据失败'); return res.json(); },