mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-14 16:26:57 +08:00
feat: update UI components and add new assets
- Update App, CompetitionPage, CryptoFeatureCard components with improvements - Enhance Header and LoginPage components - Update styling in index.css and API configurations - Add new hand background and hand image assets - Remove old logo.png file - Update server configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
@@ -88,12 +88,16 @@ func (s *Server) setupRoutes() {
|
|||||||
// 系统提示词模板管理(无需认证)
|
// 系统提示词模板管理(无需认证)
|
||||||
api.GET("/prompt-templates", s.handleGetPromptTemplates)
|
api.GET("/prompt-templates", s.handleGetPromptTemplates)
|
||||||
api.GET("/prompt-templates/:name", s.handleGetPromptTemplate)
|
api.GET("/prompt-templates/:name", s.handleGetPromptTemplate)
|
||||||
|
|
||||||
|
// 公开的竞赛数据(无需认证)
|
||||||
|
api.GET("/traders", s.handlePublicTraderList)
|
||||||
|
api.GET("/competition", s.handlePublicCompetition)
|
||||||
|
|
||||||
// 需要认证的路由
|
// 需要认证的路由
|
||||||
protected := api.Group("/", s.authMiddleware())
|
protected := api.Group("/", s.authMiddleware())
|
||||||
{
|
{
|
||||||
// AI交易员管理
|
// AI交易员管理
|
||||||
protected.GET("/traders", s.handleTraderList)
|
protected.GET("/my-traders", s.handleTraderList)
|
||||||
protected.GET("/traders/:id/config", s.handleGetTraderConfig)
|
protected.GET("/traders/:id/config", s.handleGetTraderConfig)
|
||||||
protected.POST("/traders", s.handleCreateTrader)
|
protected.POST("/traders", s.handleCreateTrader)
|
||||||
protected.PUT("/traders/:id", s.handleUpdateTrader)
|
protected.PUT("/traders/:id", s.handleUpdateTrader)
|
||||||
@@ -115,8 +119,6 @@ func (s *Server) setupRoutes() {
|
|||||||
protected.POST("/user/signal-sources", s.handleSaveUserSignalSource)
|
protected.POST("/user/signal-sources", s.handleSaveUserSignalSource)
|
||||||
|
|
||||||
|
|
||||||
// 竞赛总览
|
|
||||||
protected.GET("/competition", s.handleCompetition)
|
|
||||||
|
|
||||||
// 指定trader的数据(使用query参数 ?trader_id=xxx)
|
// 指定trader的数据(使用query参数 ?trader_id=xxx)
|
||||||
protected.GET("/status", s.handleStatus)
|
protected.GET("/status", s.handleStatus)
|
||||||
@@ -1455,3 +1457,62 @@ func (s *Server) handleGetPromptTemplate(c *gin.Context) {
|
|||||||
"content": template.Content,
|
"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)
|
||||||
|
}
|
||||||
|
|||||||
BIN
web/public/images/hand-bg.png
Normal file
BIN
web/public/images/hand-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 175 KiB |
BIN
web/public/images/hand.png
Normal file
BIN
web/public/images/hand.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
262
web/src/App.tsx
262
web/src/App.tsx
@@ -7,6 +7,8 @@ import { LoginPage } from './components/LoginPage';
|
|||||||
import { RegisterPage } from './components/RegisterPage';
|
import { RegisterPage } from './components/RegisterPage';
|
||||||
import { CompetitionPage } from './components/CompetitionPage';
|
import { CompetitionPage } from './components/CompetitionPage';
|
||||||
import { LandingPage } from './pages/LandingPage';
|
import { LandingPage } from './pages/LandingPage';
|
||||||
|
import HeaderBar from './components/landing/HeaderBar';
|
||||||
|
import LoginModal from './components/landing/LoginModal';
|
||||||
import AILearning from './components/AILearning';
|
import AILearning from './components/AILearning';
|
||||||
import { LanguageProvider, useLanguage } from './contexts/LanguageContext';
|
import { LanguageProvider, useLanguage } from './contexts/LanguageContext';
|
||||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||||
@@ -43,30 +45,44 @@ function App() {
|
|||||||
const { user, token, logout, isLoading } = useAuth();
|
const { user, token, logout, isLoading } = useAuth();
|
||||||
const { config: systemConfig, loading: configLoading } = useSystemConfig();
|
const { config: systemConfig, loading: configLoading } = useSystemConfig();
|
||||||
const [route, setRoute] = useState(window.location.pathname);
|
const [route, setRoute] = useState(window.location.pathname);
|
||||||
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||||
|
|
||||||
// 从URL hash读取初始页面状态(支持刷新保持页面)
|
// 从URL路径读取初始页面状态(支持刷新保持页面)
|
||||||
const getInitialPage = (): Page => {
|
const getInitialPage = (): Page => {
|
||||||
|
const path = window.location.pathname;
|
||||||
const hash = window.location.hash.slice(1); // 去掉 #
|
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<Page>(getInitialPage());
|
const [currentPage, setCurrentPage] = useState<Page>(getInitialPage());
|
||||||
const [selectedTraderId, setSelectedTraderId] = useState<string | undefined>();
|
const [selectedTraderId, setSelectedTraderId] = useState<string | undefined>();
|
||||||
const [lastUpdate, setLastUpdate] = useState<string>('--:--:--');
|
const [lastUpdate, setLastUpdate] = useState<string>('--:--:--');
|
||||||
|
|
||||||
// 监听URL hash变化,同步页面状态
|
// 监听URL变化,同步页面状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleHashChange = () => {
|
const handleRouteChange = () => {
|
||||||
|
const path = window.location.pathname;
|
||||||
const hash = window.location.hash.slice(1);
|
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');
|
setCurrentPage('trader');
|
||||||
} else if (hash === 'competition' || hash === '') {
|
} else if (path === '/competition' || hash === 'competition' || hash === '') {
|
||||||
setCurrentPage('competition');
|
setCurrentPage('competition');
|
||||||
}
|
}
|
||||||
|
setRoute(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('hashchange', handleHashChange);
|
window.addEventListener('hashchange', handleRouteChange);
|
||||||
return () => window.removeEventListener('hashchange', handleHashChange);
|
window.addEventListener('popstate', handleRouteChange);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('hashchange', handleRouteChange);
|
||||||
|
window.removeEventListener('popstate', handleRouteChange);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 切换页面时更新URL hash (当前通过按钮直接调用setCurrentPage,这个函数暂时保留用于未来扩展)
|
// 切换页面时更新URL hash (当前通过按钮直接调用setCurrentPage,这个函数暂时保留用于未来扩展)
|
||||||
@@ -166,153 +182,137 @@ function App() {
|
|||||||
return () => window.removeEventListener('popstate', handlePopState);
|
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
|
// Show loading spinner while checking auth or config
|
||||||
if (isLoading || configLoading) {
|
if (isLoading || configLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center" style={{ background: '#0B0E11' }}>
|
<div className="min-h-screen flex items-center justify-center" style={{ background: '#0B0E11' }}>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-16 h-16 mx-auto mb-4 animate-pulse" />
|
<img src="/icons/nofx.svg" alt="NoFx Logo" className="w-16 h-16 mx-auto mb-4 animate-pulse" />
|
||||||
<p style={{ color: '#EAECEF' }}>{t('loading', language)}</p>
|
<p style={{ color: '#EAECEF' }}>{t('loading', language)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show landing page for root route when not authenticated
|
// Handle specific routes regardless of authentication
|
||||||
|
if (route === '/login') {
|
||||||
|
return <LoginPage />;
|
||||||
|
}
|
||||||
|
if (route === '/register') {
|
||||||
|
return <RegisterPage />;
|
||||||
|
}
|
||||||
|
if (route === '/competition') {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen" style={{ background: '#000000', color: '#EAECEF' }}>
|
||||||
|
<HeaderBar
|
||||||
|
onLoginClick={() => 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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<main className="max-w-[1920px] mx-auto px-6 py-6 pt-24">
|
||||||
|
<CompetitionPage />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show landing page for root route
|
||||||
|
if (route === '/' || route === '') {
|
||||||
|
return <LandingPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show main app for authenticated users on other routes
|
||||||
if (!systemConfig?.admin_mode && (!user || !token)) {
|
if (!systemConfig?.admin_mode && (!user || !token)) {
|
||||||
if (route === '/login') {
|
// Default to landing page when not authenticated and no specific route
|
||||||
return <LoginPage />;
|
|
||||||
}
|
|
||||||
if (route === '/register') {
|
|
||||||
return <RegisterPage />;
|
|
||||||
}
|
|
||||||
// Default to landing page when not authenticated
|
|
||||||
return <LandingPage />;
|
return <LandingPage />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen" style={{ background: '#0B0E11', color: '#EAECEF' }}>
|
<div className="min-h-screen" style={{ background: 'var(--brand-black)', color: 'var(--brand-light-gray)' }}>
|
||||||
{/* Header - Binance Style */}
|
<HeaderBar
|
||||||
<header className="glass sticky top-0 z-50 backdrop-blur-xl">
|
onLoginClick={() => setShowLoginModal(true)}
|
||||||
<div className="max-w-[1920px] mx-auto px-6 py-4">
|
isLoggedIn={!!user}
|
||||||
<div className="relative flex items-center">
|
isHomePage={false}
|
||||||
{/* Left - Logo and Title */}
|
currentPage={currentPage}
|
||||||
<div className="flex items-center gap-3">
|
language={language}
|
||||||
<div className="w-8 h-8 flex items-center justify-center">
|
onLanguageChange={setLanguage}
|
||||||
<img src="/icons/nofx.svg?v=2" alt="NOFX" className="w-8 h-8" />
|
user={user}
|
||||||
</div>
|
onLogout={logout}
|
||||||
<div>
|
isAdminMode={systemConfig?.admin_mode}
|
||||||
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
onPageChange={(page) => {
|
||||||
{t('appTitle', language)}
|
console.log('App.tsx onPageChange called with:', page);
|
||||||
</h1>
|
console.log('Current route:', route, 'Current page:', currentPage);
|
||||||
<p className="text-xs mono" style={{ color: '#848E9C' }}>
|
|
||||||
{t('subtitle', language)}
|
if (page === 'competition') {
|
||||||
</p>
|
console.log('Navigating to competition');
|
||||||
</div>
|
window.history.pushState({}, '', '/competition');
|
||||||
</div>
|
setRoute('/competition');
|
||||||
|
setCurrentPage('competition');
|
||||||
{/* Center - Page Toggle (absolutely positioned) */}
|
} else if (page === 'traders') {
|
||||||
<div className="absolute left-1/2 transform -translate-x-1/2 flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
|
console.log('Navigating to traders');
|
||||||
<button
|
window.history.pushState({}, '', '/traders');
|
||||||
onClick={() => setCurrentPage('competition')}
|
setRoute('/traders');
|
||||||
className={`px-3 py-2 rounded text-sm font-semibold transition-all`}
|
setCurrentPage('traders');
|
||||||
style={currentPage === 'competition'
|
} else if (page === 'trader') {
|
||||||
? { background: '#F0B90B', color: '#000' }
|
console.log('Navigating to trader/dashboard');
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
window.history.pushState({}, '', '/dashboard');
|
||||||
}
|
setRoute('/dashboard');
|
||||||
>
|
setCurrentPage('trader');
|
||||||
{t('aiCompetition', language)}
|
}
|
||||||
</button>
|
|
||||||
<button
|
console.log('After navigation - route:', route, 'currentPage:', currentPage);
|
||||||
onClick={() => setCurrentPage('traders')}
|
}}
|
||||||
className={`px-3 py-2 rounded text-sm font-semibold transition-all`}
|
/>
|
||||||
style={currentPage === 'traders'
|
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('aiTraders', language)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setCurrentPage('trader')}
|
|
||||||
className={`px-3 py-2 rounded text-sm font-semibold transition-all`}
|
|
||||||
style={currentPage === 'trader'
|
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('tradingPanel', language)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right - Actions */}
|
|
||||||
<div className="ml-auto flex items-center gap-3">
|
|
||||||
|
|
||||||
{/* User Info - Only show if not in admin mode */}
|
|
||||||
{!systemConfig?.admin_mode && user && (
|
|
||||||
<div className="flex items-center gap-2 px-3 py-2 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
|
||||||
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold" style={{ background: '#F0B90B', color: '#000' }}>
|
|
||||||
{user.email[0].toUpperCase()}
|
|
||||||
</div>
|
|
||||||
<span className="text-sm" style={{ color: '#EAECEF' }}>{user.email}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Admin Mode Indicator */}
|
|
||||||
{systemConfig?.admin_mode && (
|
|
||||||
<div className="flex items-center gap-2 px-3 py-2 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
|
||||||
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
|
||||||
<span className="text-sm font-semibold" style={{ color: '#F0B90B' }}>{t('adminMode', language)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Language Toggle */}
|
|
||||||
<div className="flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
|
|
||||||
<button
|
|
||||||
onClick={() => setLanguage('zh')}
|
|
||||||
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
|
|
||||||
style={language === 'zh'
|
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
中文
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setLanguage('en')}
|
|
||||||
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
|
|
||||||
style={language === 'en'
|
|
||||||
? { background: '#F0B90B', color: '#000' }
|
|
||||||
: { background: 'transparent', color: '#848E9C' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
EN
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Logout Button - Only show if not in admin mode */}
|
|
||||||
{!systemConfig?.admin_mode && (
|
|
||||||
<button
|
|
||||||
onClick={logout}
|
|
||||||
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
|
|
||||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }}
|
|
||||||
>
|
|
||||||
{t('logout', language)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="max-w-[1920px] mx-auto px-6 py-6">
|
<main className="max-w-[1920px] mx-auto px-6 py-6 pt-24">
|
||||||
{currentPage === 'competition' ? (
|
{currentPage === 'competition' ? (
|
||||||
<CompetitionPage />
|
<CompetitionPage />
|
||||||
) : currentPage === 'traders' ? (
|
) : currentPage === 'traders' ? (
|
||||||
<AITradersPage
|
<AITradersPage
|
||||||
onTraderSelect={(traderId) => {
|
onTraderSelect={(traderId) => {
|
||||||
setSelectedTraderId(traderId);
|
setSelectedTraderId(traderId);
|
||||||
|
window.history.pushState({}, '', '/dashboard');
|
||||||
|
setRoute('/dashboard');
|
||||||
setCurrentPage('trader');
|
setCurrentPage('trader');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export function CompetitionPage() {
|
|||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch trader config:', error);
|
console.error('Failed to fetch trader config:', error);
|
||||||
|
// 对于未登录用户,不显示详细配置,这是正常行为
|
||||||
|
// 竞赛页面主要用于查看排行榜和基本信息
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export const CryptoFeatureCard = React.forwardRef<HTMLDivElement, CryptoFeatureC
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative h-full overflow-hidden border-2 transition-all duration-300 rounded-xl",
|
"relative h-full overflow-hidden border-2 transition-all duration-300 rounded-xl",
|
||||||
"bg-gradient-to-br from-[#0C0E12] to-[#1E2329]",
|
"bg-gradient-to-br from-[#000000] to-[#0A0A0A]",
|
||||||
"border-[#2B3139] hover:border-[#F0B90B]/50",
|
"border-[#1A1A1A] hover:border-[#F0B90B]/50",
|
||||||
isHovered && "shadow-[0_0_20px_rgba(240,185,11,0.2)]",
|
isHovered && "shadow-[0_0_20px_rgba(240,185,11,0.2)]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -61,11 +61,11 @@ export const CryptoFeatureCard = React.forwardRef<HTMLDivElement, CryptoFeatureC
|
|||||||
<div className="relative z-10 p-8 flex flex-col h-full">
|
<div className="relative z-10 p-8 flex flex-col h-full">
|
||||||
{/* Icon container */}
|
{/* Icon container */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className={cn(
|
className="mb-6 inline-flex items-center justify-center w-16 h-16 rounded-xl"
|
||||||
"mb-6 inline-flex items-center justify-center w-16 h-16 rounded-xl",
|
style={{
|
||||||
"bg-gradient-to-br from-[#F0B90B]/20 to-[#F0B90B]/5",
|
background: 'linear-gradient(135deg, rgba(240, 185, 11, 0.2) 0%, rgba(240, 185, 11, 0.05) 100%)',
|
||||||
"border border-[#F0B90B]/30"
|
border: '1px solid rgba(240, 185, 11, 0.3)'
|
||||||
)}
|
}}
|
||||||
animate={{
|
animate={{
|
||||||
scale: isHovered ? 1.1 : 1,
|
scale: isHovered ? 1.1 : 1,
|
||||||
boxShadow: isHovered
|
boxShadow: isHovered
|
||||||
@@ -74,14 +74,14 @@ export const CryptoFeatureCard = React.forwardRef<HTMLDivElement, CryptoFeatureC
|
|||||||
}}
|
}}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
>
|
>
|
||||||
<div className="text-[#F0B90B]">{icon}</div>
|
<div style={{ color: 'var(--brand-yellow)' }}>{icon}</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h3 className="text-2xl font-bold text-[#EAECEF] mb-3">{title}</h3>
|
<h3 className="text-2xl font-bold mb-3" style={{ color: 'var(--brand-light-gray)' }}>{title}</h3>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<p className="text-[#848E9C] mb-6 flex-grow leading-relaxed">{description}</p>
|
<p className="mb-6 flex-grow leading-relaxed" style={{ color: 'var(--text-secondary)' }}>{description}</p>
|
||||||
|
|
||||||
{/* Features list */}
|
{/* Features list */}
|
||||||
<div className="space-y-3 mb-6">
|
<div className="space-y-3 mb-6">
|
||||||
@@ -95,11 +95,11 @@ export const CryptoFeatureCard = React.forwardRef<HTMLDivElement, CryptoFeatureC
|
|||||||
className="flex items-start gap-3"
|
className="flex items-start gap-3"
|
||||||
>
|
>
|
||||||
<div className="mt-0.5 flex-shrink-0">
|
<div className="mt-0.5 flex-shrink-0">
|
||||||
<div className="w-5 h-5 rounded-full bg-[#F0B90B]/20 flex items-center justify-center">
|
<div className="w-5 h-5 rounded-full flex items-center justify-center" style={{ background: 'rgba(240, 185, 11, 0.2)' }}>
|
||||||
<Check className="w-3 h-3 text-[#F0B90B]" />
|
<Check className="w-3 h-3" style={{ color: 'var(--brand-yellow)' }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-[#EAECEF]">{feature}</span>
|
<span className="text-sm" style={{ color: 'var(--brand-light-gray)' }}>{feature}</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function Header({ simple = false }: HeaderProps) {
|
|||||||
{/* Left - Logo and Title */}
|
{/* Left - Logo and Title */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-8 h-8" />
|
<img src="/icons/nofx.svg" alt="NoFx Logo" className="w-8 h-8" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
import { t } from '../i18n/translations';
|
import { t } from '../i18n/translations';
|
||||||
import { Header } from './Header';
|
import HeaderBar from './landing/HeaderBar';
|
||||||
import { ArrowLeft } from 'lucide-react';
|
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const { language } = useLanguage();
|
const { language } = useLanguage();
|
||||||
@@ -51,43 +50,44 @@ export function LoginPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen" style={{ background: '#0B0E11' }}>
|
<div className="min-h-screen" style={{ background: 'var(--brand-black)' }}>
|
||||||
<Header simple />
|
<HeaderBar
|
||||||
|
onLoginClick={() => {}}
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex items-center justify-center" style={{ minHeight: 'calc(100vh - 80px)' }}>
|
<div className="flex items-center justify-center pt-20" style={{ minHeight: 'calc(100vh - 80px)' }}>
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
{/* Back to Home */}
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
window.history.pushState({}, '', '/');
|
|
||||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 mb-6 text-sm hover:text-[#F0B90B] transition-colors"
|
|
||||||
style={{ color: '#848E9C' }}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="w-4 h-4" />
|
|
||||||
返回首页
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
<div className="w-16 h-16 mx-auto mb-4 flex items-center justify-center">
|
||||||
<img src="/images/logo.png" alt="NoFx Logo" className="w-16 h-16 object-contain" />
|
<img src="/icons/nofx.svg" alt="NoFx Logo" className="w-16 h-16 object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
<h1 className="text-2xl font-bold" style={{ color: 'var(--brand-light-gray)' }}>
|
||||||
{t('loginTitle', language)}
|
登录 NOFX
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm mt-2" style={{ color: '#848E9C' }}>
|
<p className="text-sm mt-2" style={{ color: 'var(--text-secondary)' }}>
|
||||||
{step === 'login' ? t('loginTitle', language) : t('enterOTPCode', language)}
|
{step === 'login' ? '请输入您的邮箱和密码' : '请输入两步验证码'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Login Form */}
|
{/* Login Form */}
|
||||||
<div className="rounded-lg p-6" style={{ background: '#1E2329', border: '1px solid #2B3139' }}>
|
<div className="rounded-lg p-6" style={{ background: 'var(--panel-bg)', border: '1px solid var(--panel-border)' }}>
|
||||||
{step === 'login' ? (
|
{step === 'login' ? (
|
||||||
<form onSubmit={handleLogin} className="space-y-4">
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
|
||||||
{t('email', language)}
|
{t('email', language)}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -95,14 +95,14 @@ export function LoginPage() {
|
|||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded"
|
className="w-full px-3 py-2 rounded"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
|
||||||
placeholder={t('emailPlaceholder', language)}
|
placeholder={t('emailPlaceholder', language)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
|
||||||
{t('password', language)}
|
{t('password', language)}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -110,14 +110,14 @@ export function LoginPage() {
|
|||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="w-full px-3 py-2 rounded"
|
className="w-full px-3 py-2 rounded"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
|
||||||
placeholder={t('passwordPlaceholder', language)}
|
placeholder={t('passwordPlaceholder', language)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="text-sm px-3 py-2 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}>
|
<div className="text-sm px-3 py-2 rounded" style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -126,7 +126,7 @@ export function LoginPage() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50"
|
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)}
|
{loading ? t('loading', language) : t('loginButton', language)}
|
||||||
</button>
|
</button>
|
||||||
@@ -142,7 +142,7 @@ export function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm font-semibold mb-2" style={{ color: 'var(--brand-light-gray)' }}>
|
||||||
{t('otpCode', language)}
|
{t('otpCode', language)}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -150,7 +150,7 @@ export function LoginPage() {
|
|||||||
value={otpCode}
|
value={otpCode}
|
||||||
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||||
className="w-full px-3 py-2 rounded text-center text-2xl font-mono"
|
className="w-full px-3 py-2 rounded text-center text-2xl font-mono"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
style={{ background: 'var(--brand-black)', border: '1px solid var(--panel-border)', color: 'var(--brand-light-gray)' }}
|
||||||
placeholder={t('otpPlaceholder', language)}
|
placeholder={t('otpPlaceholder', language)}
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
required
|
required
|
||||||
@@ -158,7 +158,7 @@ export function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="text-sm px-3 py-2 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}>
|
<div className="text-sm px-3 py-2 rounded" style={{ background: 'var(--binance-red-bg)', color: 'var(--binance-red)' }}>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -168,7 +168,7 @@ export function LoginPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setStep('login')}
|
onClick={() => setStep('login')}
|
||||||
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
|
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)}
|
{t('back', language)}
|
||||||
</button>
|
</button>
|
||||||
@@ -187,17 +187,17 @@ export function LoginPage() {
|
|||||||
|
|
||||||
{/* Register Link */}
|
{/* Register Link */}
|
||||||
<div className="text-center mt-6">
|
<div className="text-center mt-6">
|
||||||
<p className="text-sm" style={{ color: '#848E9C' }}>
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
{t('noAccount', language)}{' '}
|
还没有账户?{' '}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.history.pushState({}, '', '/register');
|
window.history.pushState({}, '', '/register');
|
||||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||||
}}
|
}}
|
||||||
className="font-semibold hover:underline"
|
className="font-semibold hover:underline transition-colors"
|
||||||
style={{ color: '#F0B90B' }}
|
style={{ color: 'var(--brand-yellow)' }}
|
||||||
>
|
>
|
||||||
{t('registerNow', language)}
|
立即注册
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,11 +5,16 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* 强制显示滚动条以确保所有页面布局一致 */
|
||||||
|
html {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Binance Brand Colors */
|
/* Binance Brand Colors */
|
||||||
--brand-yellow: #F0B90B;
|
--brand-yellow: #F0B90B;
|
||||||
--brand-black: #0C0E12;
|
--brand-black: #000000;
|
||||||
--brand-dark-gray: #1E2329;
|
--brand-dark-gray: #0A0A0A;
|
||||||
--brand-light-gray: #EAECEF;
|
--brand-light-gray: #EAECEF;
|
||||||
--brand-almost-white: #FAFAFA;
|
--brand-almost-white: #FAFAFA;
|
||||||
--brand-white: #FFFFFF;
|
--brand-white: #FFFFFF;
|
||||||
@@ -20,14 +25,14 @@
|
|||||||
--binance-yellow-light: #FCD535;
|
--binance-yellow-light: #FCD535;
|
||||||
--binance-yellow-glow: rgba(240, 185, 11, 0.2);
|
--binance-yellow-glow: rgba(240, 185, 11, 0.2);
|
||||||
|
|
||||||
--background: #181A20; /* Binance body bg */
|
--background: #000000; /* Binance body bg */
|
||||||
--header-bg: #0B0E11; /* Binance header bg */
|
--header-bg: #000000; /* Binance header bg */
|
||||||
--background-elevated: #181A20;
|
--background-elevated: #000000;
|
||||||
--foreground: #EAECEF;
|
--foreground: #EAECEF;
|
||||||
--panel-bg: #1E2329;
|
--panel-bg: #0A0A0A;
|
||||||
--panel-bg-hover: #252930;
|
--panel-bg-hover: #111111;
|
||||||
--panel-border: #2B3139;
|
--panel-border: #1A1A1A;
|
||||||
--panel-border-hover: #474D57;
|
--panel-border-hover: #2A2A2A;
|
||||||
|
|
||||||
/* Binance Signature Colors */
|
/* Binance Signature Colors */
|
||||||
--binance-green: #0ECB81;
|
--binance-green: #0ECB81;
|
||||||
@@ -44,7 +49,7 @@
|
|||||||
--text-disabled: #474D57;
|
--text-disabled: #474D57;
|
||||||
|
|
||||||
/* Chart Colors */
|
/* Chart Colors */
|
||||||
--grid-stroke: #2B3139;
|
--grid-stroke: #1A1A1A;
|
||||||
--axis-tick: #5E6673;
|
--axis-tick: #5E6673;
|
||||||
--ref-line: #474D57;
|
--ref-line: #474D57;
|
||||||
|
|
||||||
|
|||||||
@@ -32,13 +32,20 @@ function getAuthHeaders(): Record<string, string> {
|
|||||||
export const api = {
|
export const api = {
|
||||||
// AI交易员管理接口
|
// AI交易员管理接口
|
||||||
async getTraders(): Promise<TraderInfo[]> {
|
async getTraders(): Promise<TraderInfo[]> {
|
||||||
const res = await fetch(`${API_BASE}/traders`, {
|
const res = await fetch(`${API_BASE}/my-traders`, {
|
||||||
headers: getAuthHeaders(),
|
headers: getAuthHeaders(),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('获取trader列表失败');
|
if (!res.ok) throw new Error('获取trader列表失败');
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 获取公开的交易员列表(无需认证)
|
||||||
|
async getPublicTraders(): Promise<any[]> {
|
||||||
|
const res = await fetch(`${API_BASE}/traders`);
|
||||||
|
if (!res.ok) throw new Error('获取公开trader列表失败');
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
|
||||||
async createTrader(request: CreateTraderRequest): Promise<TraderInfo> {
|
async createTrader(request: CreateTraderRequest): Promise<TraderInfo> {
|
||||||
const res = await fetch(`${API_BASE}/traders`, {
|
const res = await fetch(`${API_BASE}/traders`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -252,11 +259,9 @@ export const api = {
|
|||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取竞赛数据
|
// 获取竞赛数据(无需认证)
|
||||||
async getCompetition(): Promise<CompetitionData> {
|
async getCompetition(): Promise<CompetitionData> {
|
||||||
const res = await fetch(`${API_BASE}/competition`, {
|
const res = await fetch(`${API_BASE}/competition`);
|
||||||
headers: getAuthHeaders(),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error('获取竞赛数据失败');
|
if (!res.ok) throw new Error('获取竞赛数据失败');
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user