149 lines
4.5 KiB
TypeScript
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 };
|