Files
PolyHermes/frontend/src/components/Layout.tsx
T

307 lines
7.7 KiB
TypeScript
Raw Normal View History

2025-12-01 23:03:17 +08:00
import { useState, useEffect } from 'react'
2025-11-21 04:32:08 +08:00
import { useNavigate, useLocation } from 'react-router-dom'
import { Layout as AntLayout, Menu, Drawer, Button, Modal } from 'antd'
2025-11-21 04:32:08 +08:00
import { useMediaQuery } from 'react-responsive'
import {
WalletOutlined,
UserOutlined,
UnorderedListOutlined,
BarChartOutlined,
2025-12-01 23:03:17 +08:00
MenuOutlined,
FileTextOutlined,
LinkOutlined,
AppstoreOutlined,
TeamOutlined,
LogoutOutlined,
2025-12-03 02:27:05 +08:00
SettingOutlined,
GithubOutlined,
TwitterOutlined
2025-11-21 04:32:08 +08:00
} from '@ant-design/icons'
2025-12-01 23:03:17 +08:00
import type { MenuProps } from 'antd'
2025-11-21 04:32:08 +08:00
import type { ReactNode } from 'react'
import { removeToken } from '../utils'
import { wsManager } from '../services/websocket'
2025-11-21 04:32:08 +08:00
const { Header, Content, Sider } = AntLayout
interface LayoutProps {
children: ReactNode
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
const navigate = useNavigate()
const location = useLocation()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
2025-12-01 23:03:17 +08:00
// 获取当前选中的菜单项
const getSelectedKeys = (): string[] => {
return [location.pathname]
}
// 获取当前应该打开的父菜单
const getInitialOpenKeys = (): string[] => {
const path = location.pathname
const keys: string[] = []
2025-12-02 22:22:39 +08:00
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
keys.push('/copy-trading-management')
2025-12-01 23:03:17 +08:00
}
return keys
2025-12-01 23:03:17 +08:00
}
const [openKeys, setOpenKeys] = useState<string[]>(getInitialOpenKeys())
// 当路径变化时,自动打开对应的父菜单
useEffect(() => {
const path = location.pathname
const keys: string[] = []
2025-12-02 22:22:39 +08:00
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
keys.push('/copy-trading-management')
2025-12-01 23:03:17 +08:00
}
setOpenKeys(keys)
2025-12-01 23:03:17 +08:00
}, [location.pathname])
const menuItems: MenuProps['items'] = [
2025-11-21 04:32:08 +08:00
{
key: '/accounts',
icon: <WalletOutlined />,
label: '账户管理'
},
{
2025-12-01 23:03:17 +08:00
key: '/copy-trading-management',
icon: <AppstoreOutlined />,
2025-12-02 22:22:39 +08:00
label: '跟单交易',
2025-12-01 23:03:17 +08:00
children: [
2025-12-02 22:22:39 +08:00
{
key: '/leaders',
icon: <UserOutlined />,
label: 'Leader 管理'
},
2025-12-01 23:03:17 +08:00
{
key: '/templates',
icon: <FileTextOutlined />,
label: '跟单模板'
},
{
key: '/copy-trading',
icon: <LinkOutlined />,
label: '跟单配置'
}
]
2025-11-21 04:32:08 +08:00
},
{
key: '/positions',
2025-11-21 04:32:08 +08:00
icon: <UnorderedListOutlined />,
label: '仓位管理'
2025-11-21 04:32:08 +08:00
},
{
key: '/statistics',
icon: <BarChartOutlined />,
label: '统计信息'
},
{
key: '/users',
icon: <TeamOutlined />,
label: '用户管理'
},
{
key: '/system-settings',
icon: <SettingOutlined />,
label: '系统管理'
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录'
2025-11-21 04:32:08 +08:00
}
]
const handleLogout = () => {
removeToken()
// 断开 WebSocket 连接
wsManager.disconnect()
navigate('/login', { replace: true })
}
const handleLogoutConfirm = () => {
Modal.confirm({
title: '确认退出',
content: '确定要退出登录吗?',
okText: '确定',
cancelText: '取消',
onOk: () => {
handleLogout()
if (isMobile) {
setMobileMenuOpen(false)
}
}
})
}
2025-12-01 23:03:17 +08:00
const handleMenuClick = ({ key }: { key: string }) => {
// 如果是父菜单,不导航
if (key === '/copy-trading-management') {
return
}
// 处理退出登录
if (key === 'logout') {
handleLogoutConfirm()
return
}
2025-11-21 04:32:08 +08:00
navigate(key)
if (isMobile) {
setMobileMenuOpen(false)
}
}
2025-12-01 23:03:17 +08:00
const handleOpenChange = (keys: string[]) => {
setOpenKeys(keys)
}
2025-11-21 04:32:08 +08:00
if (isMobile) {
// 移动端布局
return (
<AntLayout style={{ minHeight: '100vh' }}>
<Header style={{
background: '#001529',
padding: '0 16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<div style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold' }}>
2025-12-02 22:22:39 +08:00
PolyHermes
2025-11-21 04:32:08 +08:00
</div>
2025-12-03 02:27:05 +08:00
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<a
href="https://github.com/WrBug/PolyHermes"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#fff', fontSize: '18px' }}
>
<GithubOutlined />
</a>
<a
href="https://x.com/quant_tr"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#fff', fontSize: '18px' }}
>
<TwitterOutlined />
</a>
<Button
type="text"
icon={<MenuOutlined />}
style={{ color: '#fff' }}
onClick={() => setMobileMenuOpen(true)}
/>
</div>
2025-11-21 04:32:08 +08:00
</Header>
<Content style={{
padding: '12px 8px',
background: '#f0f2f5',
minHeight: 'calc(100vh - 64px)'
}}>
{children}
</Content>
<Drawer
title="导航菜单"
placement="left"
onClose={() => setMobileMenuOpen(false)}
open={mobileMenuOpen}
bodyStyle={{ padding: 0 }}
>
<Menu
mode="inline"
2025-12-01 23:03:17 +08:00
selectedKeys={getSelectedKeys()}
openKeys={openKeys}
onOpenChange={handleOpenChange}
2025-11-21 04:32:08 +08:00
items={menuItems}
2025-12-01 23:03:17 +08:00
onClick={handleMenuClick}
2025-11-21 04:32:08 +08:00
style={{ border: 'none' }}
/>
</Drawer>
</AntLayout>
)
}
// 桌面端布局
return (
<AntLayout style={{ height: '100vh', overflow: 'hidden' }}>
<Sider
width={200}
style={{
background: '#001529',
height: '100vh',
position: 'fixed',
left: 0,
top: 0,
overflow: 'hidden'
}}
>
2025-11-21 04:32:08 +08:00
<div style={{
height: '64px',
display: 'flex',
alignItems: 'center',
2025-12-03 02:27:05 +08:00
justifyContent: 'space-between',
padding: '0 16px',
2025-11-21 04:32:08 +08:00
color: '#fff',
fontSize: '18px',
fontWeight: 'bold',
flexShrink: 0
2025-11-21 04:32:08 +08:00
}}>
2025-12-03 02:27:05 +08:00
<span>PolyHermes</span>
<div style={{ display: 'flex', gap: '12px' }}>
<a
href="https://github.com/WrBug/PolyHermes"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#fff', fontSize: '16px' }}
title="GitHub"
>
<GithubOutlined />
</a>
<a
href="https://x.com/quant_tr"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#fff', fontSize: '16px' }}
title="Twitter"
>
<TwitterOutlined />
</a>
</div>
2025-11-21 04:32:08 +08:00
</div>
<Menu
mode="inline"
2025-12-01 23:03:17 +08:00
selectedKeys={getSelectedKeys()}
openKeys={openKeys}
onOpenChange={handleOpenChange}
2025-11-21 04:32:08 +08:00
items={menuItems}
2025-12-01 23:03:17 +08:00
onClick={handleMenuClick}
style={{
height: 'calc(100vh - 64px)',
borderRight: 0,
overflowY: 'auto'
}}
2025-11-21 04:32:08 +08:00
/>
</Sider>
<AntLayout style={{ marginLeft: 200, height: '100vh' }}>
<Content style={{
padding: '24px',
background: '#f0f2f5',
height: '100vh',
overflowY: 'auto'
}}>
2025-11-21 04:32:08 +08:00
{children}
</Content>
</AntLayout>
</AntLayout>
)
}
export default Layout