import { useState, useEffect } from 'react' import { useNavigate, useLocation } from 'react-router-dom' import { Layout as AntLayout, Menu, Drawer, Button, Modal, Tag } from 'antd' import { useTranslation } from 'react-i18next' import { useMediaQuery } from 'react-responsive' import { WalletOutlined, UserOutlined, UnorderedListOutlined, BarChartOutlined, MenuOutlined, FileTextOutlined, LinkOutlined, AppstoreOutlined, TeamOutlined, LogoutOutlined, SettingOutlined, GithubOutlined, TwitterOutlined, CheckCircleOutlined, SendOutlined, ApiOutlined, NotificationOutlined, LineChartOutlined, RocketOutlined, DashboardOutlined } from '@ant-design/icons' import type { MenuProps } from 'antd' import type { ReactNode } from 'react' import { removeToken, getVersionText, getVersionInfo } from '../utils' import { wsManager } from '../services/websocket' import { apiClient } from '../services/api' import Logo from './Logo' const { Header, Content, Sider } = AntLayout // 添加动画样式 const style = document.createElement('style') style.textContent = ` @keyframes versionUpdatePulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(1.1); } } ` if (!document.head.querySelector('style[data-version-update-animation]')) { style.setAttribute('data-version-update-animation', 'true') document.head.appendChild(style) } interface LayoutProps { children: ReactNode } const Layout: React.FC = ({ children }) => { const { t } = useTranslation() const navigate = useNavigate() const location = useLocation() const isMobile = useMediaQuery({ maxWidth: 768 }) const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [hasUpdate, setHasUpdate] = useState(false) // 获取当前选中的菜单项 const getSelectedKeys = (): string[] => { return [location.pathname] } // 获取当前应该打开的父菜单 const getInitialOpenKeys = (): string[] => { const path = location.pathname const keys: string[] = [] if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading') || path.startsWith('/backtest')) { keys.push('/copy-trading-management') } if (path.startsWith('/crypto-tail-strategy') || path.startsWith('/crypto-tail-monitor')) { keys.push('/crypto-tail-management') } if (path.startsWith('/system-settings')) { keys.push('/system-settings') } return keys } const [openKeys, setOpenKeys] = useState(getInitialOpenKeys()) // 当路径变化时,自动打开对应的父菜单 useEffect(() => { const path = location.pathname const keys: string[] = [] if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading') || path.startsWith('/backtest')) { keys.push('/copy-trading-management') } if (path.startsWith('/crypto-tail-strategy') || path.startsWith('/crypto-tail-monitor')) { keys.push('/crypto-tail-management') } if (path.startsWith('/system-settings')) { keys.push('/system-settings') } setOpenKeys(keys) }, [location.pathname]) // 检查是否有新版本 useEffect(() => { const checkUpdate = async () => { try { const response = await apiClient.get('/update/check') if (response.data.code === 0 && response.data.data) { setHasUpdate(response.data.data.hasUpdate || false) } } catch (error) { // 静默失败,不影响页面使用 console.debug('检查更新失败:', error) } } // 页面加载时检查一次 checkUpdate() // 每5分钟检查一次 const interval = setInterval(checkUpdate, 5 * 60 * 1000) return () => clearInterval(interval) }, []) const menuItems: MenuProps['items'] = [ { key: '/announcements', icon: , label: t('menu.announcements') || '公告' }, { key: '/accounts', icon: , label: t('menu.accounts') }, { key: '/copy-trading-management', icon: , label: t('menu.copyTrading'), children: [ { key: '/copy-trading', icon: , label: t('menu.copyTradingConfig') }, { key: '/leaders', icon: , label: t('menu.leaders') }, { key: '/templates', icon: , label: t('menu.templates') }, { key: '/backtest', icon: , label: t('menu.backtest') || '回测' } ] }, { key: '/crypto-tail-management', icon: , label: t('menu.cryptoSpreadStrategy'), children: [ { key: '/crypto-tail-strategy', icon: , label: t('menu.cryptoTailStrategy') }, { key: '/crypto-tail-monitor', icon: , label: t('menu.cryptoTailMonitor') } ] }, { key: '/positions', icon: , label: t('menu.positions') }, { key: '/statistics', icon: , label: t('menu.statistics') }, { key: '/users', icon: , label: t('menu.users') }, { key: '/system-settings', icon: , label: t('menu.systemSettings') || '系统管理', children: [ { key: '/system-settings', icon: , label: t('menu.systemOverview') || '通用设置' }, { key: '/system-settings/notification', icon: , label: t('menu.notifications') || '消息推送设置' }, { key: '/system-settings/rpc-nodes', icon: , label: t('menu.rpcNodes') || 'RPC节点管理' }, { key: '/system-settings/api-health', icon: , label: t('menu.apiHealth') || 'API健康' } ] }, { key: 'logout', icon: , label: t('menu.logout') } ] const handleLogout = () => { removeToken() // 断开 WebSocket 连接 wsManager.disconnect() navigate('/login', { replace: true }) } const handleLogoutConfirm = () => { Modal.confirm({ title: t('menu.logoutConfirm'), content: t('menu.logoutConfirmDesc'), okText: t('common.confirm'), cancelText: t('common.cancel'), onOk: () => { handleLogout() if (isMobile) { setMobileMenuOpen(false) } } }) } const handleMenuClick = ({ key }: { key: string }) => { // 如果是父菜单,不导航(但 /system-settings 作为子菜单项时可以导航) if (key === '/copy-trading-management' || key === '/crypto-tail-management') { return } // 处理退出登录 if (key === 'logout') { handleLogoutConfirm() return } navigate(key) if (isMobile) { setMobileMenuOpen(false) } } const handleOpenChange = (keys: string[]) => { setOpenKeys(keys) } if (isMobile) { // 移动端布局 return (
{ if (hasUpdate) { navigate('/system-settings') } }} bordered={false} style={{ cursor: hasUpdate ? 'pointer' : 'default', fontSize: '8px', padding: '1px 6px', margin: 0, background: 'transparent', border: `1px solid ${hasUpdate ? '#faad14' : '#52c41a'}`, borderRadius: '4px', color: hasUpdate ? '#faad14' : '#52c41a', lineHeight: '1.4', display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle' }} title={hasUpdate ? t('systemUpdate.versionTooltipNew') : t('systemUpdate.versionTooltipLatest')} > {getVersionInfo().gitTag || `v${getVersionText()}`}
{children} setMobileMenuOpen(false)} open={mobileMenuOpen} bodyStyle={{ padding: 0 }} > ) } // 桌面端布局 return (
PolyHermes { if (hasUpdate) { navigate('/system-settings') } }} bordered={false} style={{ cursor: hasUpdate ? 'pointer' : 'default', fontSize: '8px', padding: '1px 6px', margin: 0, background: 'transparent', border: `1px solid ${hasUpdate ? '#faad14' : '#52c41a'}`, borderRadius: '4px', color: hasUpdate ? '#faad14' : '#52c41a', lineHeight: '1.4', display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle' }} title={hasUpdate ? t('systemUpdate.versionTooltipNew') : t('systemUpdate.versionTooltipLatest')} > {getVersionInfo().gitTag || `v${getVersionText()}`}
{children} ) } export default Layout