feat: init proj
This commit is contained in:
148
web/src/components/Layout.tsx
Normal file
148
web/src/components/Layout.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user