From bb7ecdd27b8dd5db6200f27180a4494ed2d4931c Mon Sep 17 00:00:00 2001 From: Ember <197652334@qq.com> Date: Mon, 3 Nov 2025 21:38:52 +0800 Subject: [PATCH 1/4] fix: improve mobile responsive layout for header and comparison chart This is a partial fix for issue #311 mobile display problems. Changes in this commit: - Add responsive header layout with separate mobile/desktop views in App.tsx - Fix language selector visibility on mobile (no longer hidden by menu) - Add responsive breakpoints to ComparisonChart stats grid (2 cols on mobile, 4 on desktop) - Adjust padding and text sizes for mobile screens - Preserve all i18n (internationalization) functionality from upstream Note: Additional components (CompetitionPage, AITradersPage) will need similar mobile responsive improvements in follow-up commits. Related to #311 Co-Authored-By: tinkle-community --- web/src/App.tsx | 120 +++++++++++++++++++++++-- web/src/components/ComparisonChart.tsx | 18 ++-- 2 files changed, 123 insertions(+), 15 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 6f785908..c7107023 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -194,8 +194,9 @@ function App() {
{/* Header - Binance Style */}
-
-
+
+ {/* Desktop Layout */} +
{/* Left - Logo and Title */}
@@ -210,7 +211,7 @@ function App() {

- + {/* Center - Page Toggle (absolutely positioned) */}
- + {/* Right - Actions */}
@@ -257,7 +258,7 @@ function App() { {user.email}
)} - + {/* Admin Mode Indicator */} {systemConfig?.admin_mode && (
@@ -302,11 +303,118 @@ function App() { )}
+ + {/* Mobile Layout */} +
+ {/* Top Row - Logo, Title and Language */} +
+
+ NOFX +
+

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

+

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

+
+
+ + {/* Language Toggle - Right side on mobile */} +
+ + +
+
+ + {/* Second Row - Page Toggle */} +
+ + + +
+ + {/* Third Row - User Info and Logout */} +
+ {/* User Info or Admin Mode */} + {!systemConfig?.admin_mode && user && ( +
+
+ {user.email[0].toUpperCase()} +
+ {user.email} +
+ )} + + {systemConfig?.admin_mode && ( +
+ + {t('adminMode', language)} +
+ )} + + {/* Logout Button */} + {!systemConfig?.admin_mode && ( + + )} +
+
{/* Main Content */} -
+
{currentPage === 'competition' ? ( ) : currentPage === 'traders' ? ( diff --git a/web/src/components/ComparisonChart.tsx b/web/src/components/ComparisonChart.tsx index e8d1fafe..ebd56305 100644 --- a/web/src/components/ComparisonChart.tsx +++ b/web/src/components/ComparisonChart.tsx @@ -313,24 +313,24 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{/* Stats */} -
-
+
+
{t('comparisonMode', language)}
-
PnL %
+
PnL %
-
+
{t('dataPoints', language)}
-
{t('count', language, {count: combinedData.length})}
+
{t('count', language, {count: combinedData.length})}
-
+
{t('currentGap', language)}
-
1 ? '#F0B90B' : '#EAECEF' }}> +
1 ? '#F0B90B' : '#EAECEF' }}> {currentGap.toFixed(2)}%
-
+
{t('displayRange', language)}
-
+
{combinedData.length > MAX_DISPLAY_POINTS ? `${t('recent', language)} ${MAX_DISPLAY_POINTS}` : t('allData', language)} From 7049903025f445f790bf381683baa7eb17f567f5 Mon Sep 17 00:00:00 2001 From: Ember <197652334@qq.com> Date: Mon, 3 Nov 2025 21:46:07 +0800 Subject: [PATCH 2/4] feat(web): comprehensive mobile responsive optimization for AI Traders and Competition pages Optimized mobile display for AITradersPage: - Header: Reduced padding (px-3 on mobile), smaller icons and text - Action buttons: Smaller on mobile (px-3, text-xs) with horizontal scroll support - Configuration cards: Responsive padding (p-3 on mobile), smaller gaps - Model/Exchange items: Smaller icons (w-7 on mobile), truncate text overflow - Trader list: Stack vertically on mobile, smaller buttons with wrapping support - Empty states: Smaller icons and text on mobile Maintained all i18n translations and preserved Binance design style. Addresses #311 Co-Authored-By: tinkle-community --- web/src/components/AITradersPage.tsx | 168 ++++++++++++------------- web/src/components/CompetitionPage.tsx | 34 ++--- 2 files changed, 101 insertions(+), 101 deletions(-) diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index 9361fc80..ffcb49a0 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -457,22 +457,22 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { }; return ( -
+
{/* Header */} -
-
-
+
+
- +
-

+

{t('aiTraders', language)} - {traders?.length || 0} {t('active', language)} @@ -482,37 +482,37 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {

- -
+ +
- +
{/* Configuration Status */} -
+
{/* AI Models */} -
-

- +
+

+ {t('aiModels', language)}

-
+
{configuredModels.map(model => { const inUse = isModelInUse(model.id); return ( -
handleModelClick(model.id)} > -
-
- {getModelIcon(model.provider || model.id, { width: 32, height: 32 }) || ( -
+
+ {getModelIcon(model.provider || model.id, { width: 28, height: 28 }) || ( +
@@ -569,63 +569,63 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
)}
-
-
{getShortName(model.name)}
+
+
{getShortName(model.name)}
{inUse ? t('inUse', language) : model.enabled ? t('enabled', language) : t('configured', language)}
-
+
); })} {configuredModels.length === 0 && ( -
- -
{t('noModelsConfigured', language)}
+
+ +
{t('noModelsConfigured', language)}
)}
{/* Exchanges */} -
-

- +
+

+ {t('exchanges', language)}

-
+
{configuredExchanges.map(exchange => { const inUse = isExchangeInUse(exchange.id); return ( -
handleExchangeClick(exchange.id)} > -
-
- {getExchangeIcon(exchange.id, { width: 32, height: 32 })} +
+
+ {getExchangeIcon(exchange.id, { width: 28, height: 28 })}
-
-
{getShortName(exchange.name)}
+
+
{getShortName(exchange.name)}
{exchange.type.toUpperCase()} • {inUse ? t('inUse', language) : exchange.enabled ? t('enabled', language) : t('configured', language)}
-
+
); })} {configuredExchanges.length === 0 && ( -
- -
{t('noExchangesConfigured', language)}
+
+ +
{t('noExchangesConfigured', language)}
)}
@@ -633,47 +633,47 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
{/* Traders List */} -
-
-

- +
+
+

+ {t('currentTraders', language)}

{traders && traders.length > 0 ? ( -
+
{traders.map(trader => (
-
-
+
- +
-
-
+
+
{trader.trader_name}
-
{getModelDisplayName(trader.ai_model.split('_').pop() || trader.ai_model)} Model • {trader.exchange_id?.toUpperCase()}
-
+
{/* Status */}
{t('status', language)}
-
@@ -682,20 +682,20 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
{/* Actions */} -
+
- +
@@ -728,15 +728,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { ))}
) : ( -
- -
{t('noTraders', language)}
-
{t('createFirstTrader', language)}
+
+ +
{t('noTraders', language)}
+
{t('createFirstTrader', language)}
{(configuredModels.length === 0 || configuredExchanges.length === 0) && ( -
- {configuredModels.length === 0 && configuredExchanges.length === 0 +
+ {configuredModels.length === 0 && configuredExchanges.length === 0 ? t('configureModelsAndExchangesFirst', language) - : configuredModels.length === 0 + : configuredModels.length === 0 ? t('configureModelsFirst', language) : t('configureExchangesFirst', language) } diff --git a/web/src/components/CompetitionPage.tsx b/web/src/components/CompetitionPage.tsx index 1ebdb564..fcd332d4 100644 --- a/web/src/components/CompetitionPage.tsx +++ b/web/src/components/CompetitionPage.tsx @@ -73,13 +73,16 @@ export function CompetitionPage() { return (
{/* Competition Header - 精简版 */} -
-
-
- +
+
+
+
-

+

{t('aiCompetition', language)} {competition.count} {t('traders', language)} @@ -90,9 +93,9 @@ export function CompetitionPage() {

-
+
{t('leader', language)}
-
{leader?.trader_name}
+
{leader?.trader_name}
= 0 ? '#0ECB81' : '#F6465D' }}> {(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
@@ -155,20 +158,20 @@ export function CompetitionPage() {
{/* Stats */} -
+
{/* Total Equity */}
{t('equity', language)}
-
+
{trader.total_equity?.toFixed(2) || '0.00'}
{/* P&L */} -
+
{t('pnl', language)}
= 0 ? '#0ECB81' : '#F6465D' }} > {(trader.total_pnl ?? 0) >= 0 ? '+' : ''} @@ -182,7 +185,7 @@ export function CompetitionPage() { {/* Positions */}
{t('pos', language)}
-
+
{trader.position_count}
@@ -242,15 +245,12 @@ export function CompetitionPage() { >
{trader.trader_name}
-
- {trader.ai_model.toUpperCase()} + {trader.exchange.toUpperCase()} -
-
= 0 ? '#0ECB81' : '#F6465D' }}> +
= 0 ? '#0ECB81' : '#F6465D' }}> {(trader.total_pnl ?? 0) >= 0 ? '+' : ''}{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
{isWinning && gap > 0 && ( From 804bb4c302d488f84bb5f09ec8faf5fd8b9f3028 Mon Sep 17 00:00:00 2001 From: icy Date: Mon, 3 Nov 2025 23:13:53 +0800 Subject: [PATCH 3/4] =?UTF-8?q?Fix=20equity-history-batch=20API=20to=20sup?= =?UTF-8?q?port=20POST=20JSON=20requests=20-=20Change=20route=20from=20GET?= =?UTF-8?q?=20to=20POST=20for=20equity-history-batch=20endpoint=20-=20Upda?= =?UTF-8?q?te=20handleEquityHistoryBatch=20to=20parse=20JSON=20body=20from?= =?UTF-8?q?=20POST=20requests=20-=20Maintain=20backward=20compatibility=20?= =?UTF-8?q?with=20GET=20query=20parameters=20-=20Ensure=20public=20access?= =?UTF-8?q?=20without=20authentication=20as=20required=20=F0=9F=A4=96=20Ge?= =?UTF-8?q?nerated=20with=20[Claude=20Code](https://claude.ai/code)=20Co-A?= =?UTF-8?q?uthored-By:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server.go | 81 ++++++++++++++++++++++++++++----------------------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/api/server.go b/api/server.go index bcf45ca9..32549793 100644 --- a/api/server.go +++ b/api/server.go @@ -94,7 +94,7 @@ func (s *Server) setupRoutes() { api.GET("/competition", s.handlePublicCompetition) api.GET("/top-traders", s.handleTopTraders) api.GET("/equity-history", s.handleEquityHistory) - api.GET("/equity-history-batch", s.handleEquityHistoryBatch) + api.POST("/equity-history-batch", s.handleEquityHistoryBatch) api.GET("/traders/:id/public-config", s.handleGetPublicTraderConfig) // 需要认证的路由 @@ -1602,49 +1602,56 @@ func (s *Server) handleTopTraders(c *gin.Context) { // handleEquityHistoryBatch 批量获取多个交易员的收益率历史数据(无需认证,用于表现对比) func (s *Server) handleEquityHistoryBatch(c *gin.Context) { - // 获取trader_ids参数,支持逗号分隔的多个ID - traderIDsParam := c.Query("trader_ids") - if traderIDsParam == "" { - // 如果没有指定trader_ids,则返回前10名的历史数据 - topTraders, err := s.traderManager.GetTopTradersData() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "error": fmt.Sprintf("获取前10名交易员失败: %v", err), - }) - return - } - - traders, ok := topTraders["traders"].([]map[string]interface{}) - if !ok { - c.JSON(http.StatusInternalServerError, gin.H{"error": "交易员数据格式错误"}) - return - } - - // 提取trader IDs - traderIDs := make([]string, 0, len(traders)) - for _, trader := range traders { - if traderID, ok := trader["trader_id"].(string); ok { - traderIDs = append(traderIDs, traderID) - } - } - - result := s.getEquityHistoryForTraders(traderIDs) - c.JSON(http.StatusOK, result) - return + var requestBody struct { + TraderIDs []string `json:"trader_ids"` } - // 解析逗号分隔的trader IDs - traderIDs := strings.Split(traderIDsParam, ",") - for i := range traderIDs { - traderIDs[i] = strings.TrimSpace(traderIDs[i]) + // 尝试解析POST请求的JSON body + if err := c.ShouldBindJSON(&requestBody); err != nil { + // 如果JSON解析失败,尝试从query参数获取(兼容GET请求) + traderIDsParam := c.Query("trader_ids") + if traderIDsParam == "" { + // 如果没有指定trader_ids,则返回前10名的历史数据 + topTraders, err := s.traderManager.GetTopTradersData() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": fmt.Sprintf("获取前10名交易员失败: %v", err), + }) + return + } + + traders, ok := topTraders["traders"].([]map[string]interface{}) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"error": "交易员数据格式错误"}) + return + } + + // 提取trader IDs + traderIDs := make([]string, 0, len(traders)) + for _, trader := range traders { + if traderID, ok := trader["trader_id"].(string); ok { + traderIDs = append(traderIDs, traderID) + } + } + + result := s.getEquityHistoryForTraders(traderIDs) + c.JSON(http.StatusOK, result) + return + } + + // 解析逗号分隔的trader IDs + requestBody.TraderIDs = strings.Split(traderIDsParam, ",") + for i := range requestBody.TraderIDs { + requestBody.TraderIDs[i] = strings.TrimSpace(requestBody.TraderIDs[i]) + } } // 限制最多20个交易员,防止请求过大 - if len(traderIDs) > 20 { - traderIDs = traderIDs[:20] + if len(requestBody.TraderIDs) > 20 { + requestBody.TraderIDs = requestBody.TraderIDs[:20] } - result := s.getEquityHistoryForTraders(traderIDs) + result := s.getEquityHistoryForTraders(requestBody.TraderIDs) c.JSON(http.StatusOK, result) } From 41b57ce83428cb5e0b533e943b1d3c91287211db Mon Sep 17 00:00:00 2001 From: icy Date: Mon, 3 Nov 2025 23:14:10 +0800 Subject: [PATCH 4/4] =?UTF-8?q?Prevent=20my-traders=20API=20calls=20when?= =?UTF-8?q?=20user=20is=20not=20logged=20in=20-=20Add=20authentication=20c?= =?UTF-8?q?hecks=20to=20SWR=20calls=20in=20App.tsx=20and=20AITradersPage.t?= =?UTF-8?q?sx=20-=20Only=20call=20api.getTraders=20when=20user=20and=20tok?= =?UTF-8?q?en=20are=20available=20-=20Modify=20loadConfigs=20to=20skip=20a?= =?UTF-8?q?uthenticated=20API=20calls=20when=20not=20logged=20in=20-=20Loa?= =?UTF-8?q?d=20only=20public=20supported=20models/exchanges=20for=20unauth?= =?UTF-8?q?enticated=20users=20-=20Update=20useEffect=20dependencies=20to?= =?UTF-8?q?=20include=20user=20and=20token=20=F0=9F=A4=96=20Generated=20wi?= =?UTF-8?q?th=20[Claude=20Code](https://claude.ai/code)=20Co-Authored-By:?= =?UTF-8?q?=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/App.tsx | 12 ++++++++---- web/src/components/AITradersPage.tsx | 21 +++++++++++++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 11e604ce..05a0af5b 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -88,10 +88,14 @@ function App() { // window.location.hash = page === 'competition' ? '' : 'trader'; // }; - // 获取trader列表 - const { data: traders } = useSWR('traders', api.getTraders, { - refreshInterval: 10000, - }); + // 获取trader列表(仅在用户登录时) + const { data: traders } = useSWR( + user && token ? 'traders' : null, + api.getTraders, + { + refreshInterval: 10000, + } + ); // 当获取到traders后,设置默认选中第一个 useEffect(() => { diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index d534c0b0..38ee4075 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -4,6 +4,7 @@ import { api } from '../lib/api'; import type { TraderInfo, CreateTraderRequest, AIModel, Exchange } from '../types'; import { useLanguage } from '../contexts/LanguageContext'; import { t, type Language } from '../i18n/translations'; +import { useAuth } from '../contexts/AuthContext'; import { getExchangeIcon } from './ExchangeIcons'; import { getModelIcon } from './ModelIcons'; import { TraderConfigModal } from './TraderConfigModal'; @@ -35,6 +36,7 @@ interface AITradersPageProps { export function AITradersPage({ onTraderSelect }: AITradersPageProps) { const { language } = useLanguage(); + const { user, token } = useAuth(); const [showCreateModal, setShowCreateModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showModelModal, setShowModelModal] = useState(false); @@ -53,7 +55,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { }); const { data: traders, mutate: mutateTraders } = useSWR( - 'traders', + user && token ? 'traders' : null, api.getTraders, { refreshInterval: 5000 } ); @@ -61,6 +63,21 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { // 加载AI模型和交易所配置 useEffect(() => { const loadConfigs = async () => { + if (!user || !token) { + // 未登录时只加载公开的支持模型和交易所 + try { + const [supportedModels, supportedExchanges] = await Promise.all([ + api.getSupportedModels(), + api.getSupportedExchanges() + ]); + setSupportedModels(supportedModels); + setSupportedExchanges(supportedExchanges); + } catch (err) { + console.error('Failed to load supported configs:', err); + } + return; + } + try { const [modelConfigs, exchangeConfigs, supportedModels, supportedExchanges] = await Promise.all([ api.getModelConfigs(), @@ -88,7 +105,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { } }; loadConfigs(); - }, []); + }, [user, token]); // 显示所有用户的模型和交易所配置(用于调试) const configuredModels = allModels || [];