mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 23:36:55 +08:00
bugfix dashboard empty state (#709)
This commit is contained in:
126
web/src/App.tsx
126
web/src/App.tsx
@@ -101,11 +101,12 @@ function App() {
|
|||||||
// };
|
// };
|
||||||
|
|
||||||
// 获取trader列表(仅在用户登录时)
|
// 获取trader列表(仅在用户登录时)
|
||||||
const { data: traders } = useSWR<TraderInfo[]>(
|
const { data: traders, error: tradersError } = useSWR<TraderInfo[]>(
|
||||||
user && token ? 'traders' : null,
|
user && token ? 'traders' : null,
|
||||||
api.getTraders,
|
api.getTraders,
|
||||||
{
|
{
|
||||||
refreshInterval: 10000,
|
refreshInterval: 10000,
|
||||||
|
shouldRetryOnError: false, // 避免在后端未运行时无限重试
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -360,8 +361,14 @@ function App() {
|
|||||||
lastUpdate={lastUpdate}
|
lastUpdate={lastUpdate}
|
||||||
language={language}
|
language={language}
|
||||||
traders={traders}
|
traders={traders}
|
||||||
|
tradersError={tradersError}
|
||||||
selectedTraderId={selectedTraderId}
|
selectedTraderId={selectedTraderId}
|
||||||
onTraderSelect={setSelectedTraderId}
|
onTraderSelect={setSelectedTraderId}
|
||||||
|
onNavigateToTraders={() => {
|
||||||
|
window.history.pushState({}, '', '/traders')
|
||||||
|
setRoute('/traders')
|
||||||
|
setCurrentPage('traders')
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
@@ -426,13 +433,17 @@ function TraderDetailsPage({
|
|||||||
lastUpdate,
|
lastUpdate,
|
||||||
language,
|
language,
|
||||||
traders,
|
traders,
|
||||||
|
tradersError,
|
||||||
selectedTraderId,
|
selectedTraderId,
|
||||||
onTraderSelect,
|
onTraderSelect,
|
||||||
|
onNavigateToTraders,
|
||||||
}: {
|
}: {
|
||||||
selectedTrader?: TraderInfo
|
selectedTrader?: TraderInfo
|
||||||
traders?: TraderInfo[]
|
traders?: TraderInfo[]
|
||||||
|
tradersError?: Error
|
||||||
selectedTraderId?: string
|
selectedTraderId?: string
|
||||||
onTraderSelect: (traderId: string) => void
|
onTraderSelect: (traderId: string) => void
|
||||||
|
onNavigateToTraders: () => void
|
||||||
status?: SystemStatus
|
status?: SystemStatus
|
||||||
account?: AccountInfo
|
account?: AccountInfo
|
||||||
positions?: Position[]
|
positions?: Position[]
|
||||||
@@ -441,6 +452,119 @@ function TraderDetailsPage({
|
|||||||
lastUpdate: string
|
lastUpdate: string
|
||||||
language: Language
|
language: Language
|
||||||
}) {
|
}) {
|
||||||
|
// If API failed with error, show empty state (likely backend not running)
|
||||||
|
if (tradersError) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<div className="text-center max-w-md mx-auto px-6">
|
||||||
|
{/* Icon */}
|
||||||
|
<div
|
||||||
|
className="w-24 h-24 mx-auto mb-6 rounded-full flex items-center justify-center"
|
||||||
|
style={{
|
||||||
|
background: 'rgba(240, 185, 11, 0.1)',
|
||||||
|
border: '2px solid rgba(240, 185, 11, 0.3)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-12 h-12"
|
||||||
|
style={{ color: '#F0B90B' }}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h2 className="text-2xl font-bold mb-3" style={{ color: '#EAECEF' }}>
|
||||||
|
{t('dashboardEmptyTitle', language)}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-base mb-6" style={{ color: '#848E9C' }}>
|
||||||
|
{t('dashboardEmptyDescription', language)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA Button */}
|
||||||
|
<button
|
||||||
|
onClick={onNavigateToTraders}
|
||||||
|
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||||
|
color: '#0B0E11',
|
||||||
|
boxShadow: '0 4px 12px rgba(240, 185, 11, 0.3)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('goToTradersPage', language)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If traders is loaded and empty, show empty state
|
||||||
|
if (traders && traders.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<div className="text-center max-w-md mx-auto px-6">
|
||||||
|
{/* Icon */}
|
||||||
|
<div
|
||||||
|
className="w-24 h-24 mx-auto mb-6 rounded-full flex items-center justify-center"
|
||||||
|
style={{
|
||||||
|
background: 'rgba(240, 185, 11, 0.1)',
|
||||||
|
border: '2px solid rgba(240, 185, 11, 0.3)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-12 h-12"
|
||||||
|
style={{ color: '#F0B90B' }}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h2 className="text-2xl font-bold mb-3" style={{ color: '#EAECEF' }}>
|
||||||
|
{t('dashboardEmptyTitle', language)}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-base mb-6" style={{ color: '#848E9C' }}>
|
||||||
|
{t('dashboardEmptyDescription', language)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA Button */}
|
||||||
|
<button
|
||||||
|
onClick={onNavigateToTraders}
|
||||||
|
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105 active:scale-95"
|
||||||
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||||
|
color: '#0B0E11',
|
||||||
|
boxShadow: '0 4px 12px rgba(240, 185, 11, 0.3)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('goToTradersPage', language)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If traders is still loading or selectedTrader is not ready, show skeleton
|
||||||
if (!selectedTrader) {
|
if (!selectedTrader) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
|
Radio,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
// 获取友好的AI模型名称
|
// 获取友好的AI模型名称
|
||||||
@@ -702,7 +703,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2 md:gap-3 w-full md:w-auto overflow-x-auto flex-wrap md:flex-nowrap">
|
<div className="flex gap-2 md:gap-3 w-full md:w-auto overflow-hidden flex-wrap md:flex-nowrap">
|
||||||
<button
|
<button
|
||||||
onClick={handleAddModel}
|
onClick={handleAddModel}
|
||||||
className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1 md:gap-2 whitespace-nowrap"
|
className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1 md:gap-2 whitespace-nowrap"
|
||||||
@@ -731,14 +732,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowSignalSourceModal(true)}
|
onClick={() => setShowSignalSourceModal(true)}
|
||||||
className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 whitespace-nowrap"
|
className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1 md:gap-2 whitespace-nowrap"
|
||||||
style={{
|
style={{
|
||||||
background: '#2B3139',
|
background: '#2B3139',
|
||||||
color: '#EAECEF',
|
color: '#EAECEF',
|
||||||
border: '1px solid #474D57',
|
border: '1px solid #474D57',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
📡 {t('signalSource', language)}
|
<Radio className="w-3 h-3 md:w-4 md:h-4" />
|
||||||
|
{t('signalSource', language)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -793,7 +795,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
<strong>{t('solutions', language)}</strong>
|
<strong>{t('solutions', language)}</strong>
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc list-inside space-y-1 ml-2 mt-1">
|
<ul className="list-disc list-inside space-y-1 ml-2 mt-1">
|
||||||
<li>点击"📡 {t('signalSource', language)}"按钮配置API地址</li>
|
<li>点击"{t('signalSource', language)}"按钮配置API地址</li>
|
||||||
<li>或在交易员配置中禁用"使用币种池"和"使用OI Top"</li>
|
<li>或在交易员配置中禁用"使用币种池"和"使用OI Top"</li>
|
||||||
<li>或在交易员配置中设置自定义币种列表</li>
|
<li>或在交易员配置中设置自定义币种列表</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -1292,7 +1294,7 @@ function SignalSourceModal({
|
|||||||
style={{ background: '#1E2329' }}
|
style={{ background: '#1E2329' }}
|
||||||
>
|
>
|
||||||
<h3 className="text-xl font-bold mb-4" style={{ color: '#EAECEF' }}>
|
<h3 className="text-xl font-bold mb-4" style={{ color: '#EAECEF' }}>
|
||||||
📡 {t('signalSourceConfig', language)}
|
{t('signalSourceConfig', language)}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ export const translations = {
|
|||||||
currentTraders: 'Current Traders',
|
currentTraders: 'Current Traders',
|
||||||
noTraders: 'No AI Traders',
|
noTraders: 'No AI Traders',
|
||||||
createFirstTrader: 'Create your first AI trader to get started',
|
createFirstTrader: 'Create your first AI trader to get started',
|
||||||
|
dashboardEmptyTitle: 'No Traders Configured',
|
||||||
|
dashboardEmptyDescription:
|
||||||
|
"You haven't created any AI traders yet. Create your first trader to start automated trading.",
|
||||||
|
goToTradersPage: 'Go to Traders Page',
|
||||||
configureModelsFirst: 'Please configure AI models first',
|
configureModelsFirst: 'Please configure AI models first',
|
||||||
configureExchangesFirst: 'Please configure exchanges first',
|
configureExchangesFirst: 'Please configure exchanges first',
|
||||||
configureModelsAndExchangesFirst:
|
configureModelsAndExchangesFirst:
|
||||||
@@ -915,6 +919,10 @@ export const translations = {
|
|||||||
currentTraders: '当前交易员',
|
currentTraders: '当前交易员',
|
||||||
noTraders: '暂无AI交易员',
|
noTraders: '暂无AI交易员',
|
||||||
createFirstTrader: '创建您的第一个AI交易员开始使用',
|
createFirstTrader: '创建您的第一个AI交易员开始使用',
|
||||||
|
dashboardEmptyTitle: '暂无交易员',
|
||||||
|
dashboardEmptyDescription:
|
||||||
|
'您还未创建任何AI交易员,创建您的第一个交易员以开始自动化交易。',
|
||||||
|
goToTradersPage: '前往交易员页面',
|
||||||
configureModelsFirst: '请先配置AI模型',
|
configureModelsFirst: '请先配置AI模型',
|
||||||
configureExchangesFirst: '请先配置交易所',
|
configureExchangesFirst: '请先配置交易所',
|
||||||
configureModelsAndExchangesFirst: '请先配置AI模型和交易所',
|
configureModelsAndExchangesFirst: '请先配置AI模型和交易所',
|
||||||
|
|||||||
Reference in New Issue
Block a user