TradingAgents/web_dashboard/frontend/src/App.jsx

218 lines
7.1 KiB
JavaScript

import { useState, useEffect, lazy, Suspense } from 'react'
import { Routes, Route, NavLink, useLocation, useNavigate } from 'react-router-dom'
import {
FundOutlined,
MonitorOutlined,
FileTextOutlined,
ClusterOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
WalletOutlined,
} from '@ant-design/icons'
const ScreeningPanel = lazy(() => import('./pages/ScreeningPanel'))
const AnalysisMonitor = lazy(() => import('./pages/AnalysisMonitor'))
const ReportsViewer = lazy(() => import('./pages/ReportsViewer'))
const BatchManager = lazy(() => import('./pages/BatchManager'))
const PortfolioPanel = lazy(() => import('./pages/PortfolioPanel'))
const SetupWizard = lazy(() => import('./pages/SetupWizard'))
const navItems = [
{ path: '/', icon: <FundOutlined />, label: '筛选', key: '1' },
{ path: '/monitor', icon: <MonitorOutlined />, label: '监控', key: '2' },
{ path: '/reports', icon: <FileTextOutlined />, label: '报告', key: '3' },
{ path: '/batch', icon: <ClusterOutlined />, label: '批量', key: '4' },
{ path: '/portfolio', icon: <WalletOutlined />, label: '组合', key: '5' },
]
function Layout({ children }) {
const [collapsed, setCollapsed] = useState(false)
const [isMobile, setIsMobile] = useState(false)
const location = useLocation()
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768)
checkMobile()
window.addEventListener('resize', checkMobile)
return () => window.removeEventListener('resize', checkMobile)
}, [])
const currentPage = navItems.find(item =>
item.path === '/'
? location.pathname === '/'
: location.pathname.startsWith(item.path)
)?.label || 'TradingAgents'
return (
<div className="dashboard-layout">
{/* Sidebar - Apple Glass Navigation */}
{!isMobile && (
<aside className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
<div className="sidebar-logo">
{!collapsed && <span>TradingAgents</span>}
{collapsed && <span style={{ fontSize: 12, letterSpacing: '0.1em' }}>TA</span>}
</div>
<nav className="sidebar-nav">
{navItems.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
`nav-item ${isActive ? 'active' : ''}`
}
end={item.path === '/'}
aria-label={`${item.label} (按${item.key}切换)`}
>
{item.icon}
{!collapsed && <span>{item.label}</span>}
</NavLink>
))}
</nav>
<div style={{ padding: 'var(--space-2)' }}>
<button
onClick={() => setCollapsed(!collapsed)}
aria-label={collapsed ? '展开侧边栏' : '收起侧边栏'}
className="sidebar-collapse-btn"
>
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
{!collapsed && <span>收起</span>}
</button>
</div>
</aside>
)}
{/* Main Content */}
<main className={`main-content ${collapsed && !isMobile ? 'sidebar-collapsed' : ''}`}>
{!isMobile && (
<header className="topbar">
<div className="topbar-title">{currentPage}</div>
<div className="topbar-date">
{new Date().toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</div>
</header>
)}
<div className="page-content">
{children}
</div>
</main>
{/* Mobile TabBar */}
{isMobile && (
<nav className="mobile-tabbar" aria-label="移动端导航">
{navItems.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
`mobile-tab-item ${isActive ? 'active' : ''}`
}
end={item.path === '/'}
aria-label={item.label}
>
<span className="mobile-tab-icon">{item.icon}</span>
<span className="mobile-tab-label">{item.label}</span>
</NavLink>
))}
</nav>
)}
</div>
)
}
export default function App() {
const navigate = useNavigate()
const [configured, setConfigured] = useState(null) // null = checking, true/false
// Check if API key is configured on mount
useEffect(() => {
const checkConfig = async () => {
try {
// Check via Tauri command first (desktop app)
if (window.__TAURI__) {
const { invoke } = window.__TAURI__.core
const isConfigured = await invoke('is_configured')
setConfigured(isConfigured)
} else {
// Fallback: call backend API
const res = await fetch('/api/config/check')
const data = await res.json()
setConfigured(data.configured)
}
} catch (e) {
// Backend might not be ready yet, assume not configured
setConfigured(false)
}
}
checkConfig()
}, [])
useEffect(() => {
const handleKeyDown = (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
// Close modals on Escape
if (e.key === 'Escape') {
document.querySelector('.ant-modal-wrap')?.click()
return
}
// Navigation shortcuts
switch (e.key) {
case '1': navigate('/'); break
case '2': navigate('/monitor'); break
case '3': navigate('/reports'); break
case '4': navigate('/batch'); break
case '5': navigate('/portfolio'); break
default: break
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [navigate])
// Still checking config
if (configured === null) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', background: 'var(--bg-primary)' }}>
<div className="loading-pulse">加载中...</div>
</div>
)
}
// Not configured - show setup wizard
if (!configured) {
return (
<Suspense fallback={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', background: 'var(--bg-primary)' }}>
<div className="loading-pulse">加载中...</div>
</div>
}>
<SetupWizard onComplete={() => setConfigured(true)} />
</Suspense>
)
}
return (
<Layout>
<Suspense fallback={
<div style={{ padding: 'var(--space-12)', textAlign: 'center' }}>
<div className="loading-pulse">加载中...</div>
</div>
}>
<Routes>
<Route path="/" element={<ScreeningPanel />} />
<Route path="/monitor" element={<AnalysisMonitor />} />
<Route path="/reports" element={<ReportsViewer />} />
<Route path="/batch" element={<BatchManager />} />
<Route path="/portfolio" element={<PortfolioPanel />} />
</Routes>
</Suspense>
</Layout>
)
}