Files
sub-router/web/src/components/Layout.tsx
2026-03-31 13:11:54 +08:00

149 lines
4.5 KiB
TypeScript

import { useState, useEffect } from 'react';
import { stats as statsApi } from '../api';
type Panel = 'subscriptions' | 'static-nodes' | 'node-selector' | 'rules' | 'output';
const NAV_ITEMS: { key: Panel; label: string; icon: string }[] = [
{ key: 'subscriptions', label: '订阅', icon: '⟐' },
{ key: 'static-nodes', label: '节点', icon: '◈' },
{ key: 'node-selector', label: '选择', icon: '☰' },
{ key: 'rules', label: '规则', icon: '⧖' },
{ key: 'output', label: '输出', icon: '▸' },
];
interface LayoutProps {
activePanel: Panel;
onPanelChange: (panel: Panel) => void;
children: React.ReactNode;
}
export default function Layout({ activePanel, onPanelChange, children }: LayoutProps) {
const [statsData, setStatsData] = useState<any>(null);
useEffect(() => {
const load = () => statsApi.get().then(setStatsData).catch(() => {});
load();
const interval = setInterval(load, 10000);
return () => clearInterval(interval);
}, []);
const totalNodes = statsData
? statsData.nodes.fetched.enabled + statsData.nodes.static.enabled
: 0;
const totalNodesAll = statsData
? statsData.nodes.fetched.total + statsData.nodes.static.total
: 0;
return (
<div style={{
display: 'flex',
flexDirection: 'column',
height: '100vh',
background: 'var(--bg-primary)',
}}>
{/* Header */}
<header style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 20px',
height: 40,
borderBottom: '1px solid var(--border)',
background: 'var(--bg-panel)',
flexShrink: 0,
}}>
<div style={{
fontFamily: 'var(--font-mono)',
fontWeight: 700,
fontSize: 14,
letterSpacing: '0.1em',
color: 'var(--accent)',
}}>
Sub Router
</div>
<div style={{
fontFamily: 'var(--font-mono)',
fontSize: 11,
color: 'var(--text-muted)',
}}>
v1.0.0
</div>
</header>
{/* Main */}
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* Sidebar */}
<nav style={{
width: 72,
background: 'var(--bg-panel)',
borderRight: '1px solid var(--border)',
display: 'flex',
flexDirection: 'column',
paddingTop: 8,
flexShrink: 0,
}}>
{NAV_ITEMS.map(item => (
<button
key={item.key}
onClick={() => onPanelChange(item.key)}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 4,
padding: '12px 0',
margin: '2px 6px',
border: 'none',
borderRadius: 'var(--radius)',
background: activePanel === item.key ? 'var(--bg-active)' : 'transparent',
color: activePanel === item.key ? 'var(--accent)' : 'var(--text-secondary)',
cursor: 'pointer',
transition: 'all var(--transition)',
textTransform: 'none',
letterSpacing: 'normal',
fontFamily: 'var(--font-sans)',
fontSize: 10,
fontWeight: activePanel === item.key ? 600 : 400,
}}
>
<span style={{ fontSize: 18, lineHeight: 1 }}>{item.icon}</span>
<span>{item.label}</span>
</button>
))}
</nav>
{/* Content */}
<main style={{
flex: 1,
overflow: 'auto',
padding: 24,
}}>
{children}
</main>
</div>
{/* Status bar */}
<footer style={{
display: 'flex',
alignItems: 'center',
gap: 24,
padding: '0 16px',
height: 28,
borderTop: '1px solid var(--border)',
background: 'var(--bg-panel)',
fontFamily: 'var(--font-mono)',
fontSize: 11,
color: 'var(--text-secondary)',
flexShrink: 0,
}}>
<span style={{ color: 'var(--success)' }}> online</span>
<span>nodes: <span style={{ color: 'var(--accent)' }}>{totalNodes}</span>/{totalNodesAll}</span>
<span>rules: <span style={{ color: 'var(--accent)' }}>{statsData?.rules ?? 0}</span></span>
<span>port: <span style={{ color: 'var(--accent)' }}>3456</span></span>
</footer>
</div>
);
}
export type { Panel };